﻿/*
____
 L|ypps Web controls for ASP.NET

Copyright (c) 2009 Alessandro Zifiglio, http://www.Typps.com
EMail: alessandro@Typps.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
namespace Typps
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    public class HtmlFormatter
    {
        #region Fields

        bool addToScriptBlock = false;
        bool appendToEndElement = false;
        bool appendToStartElement = false;
        string attribute = string.Empty;
        List<string> attributes = null;
        bool beginAttribute = false;
        bool concatScriptBlockValue = true;
        bool endAttribute = false;
        string endTag = string.Empty;
        bool ignore = false;
        List<string> ignoreElements = new List<string>(new string[] { "style", "script", "pre", "code" });
        int indent = 0;
        string lastChar = string.Empty;
        CharacterLocation location = CharacterLocation.OuterHTML;
        bool moveScriptBlock = true;
        List<string> noFormattingListValue;
        List<string> openElementsHierarchy = null;
        bool removeFormattingAndSpacesValue = false;
        StringBuilder scriptBlocks = new StringBuilder();
        List<string> scriptIdIgnoreMarkerValue = null;
        List<string> scriptInclude = new List<string>();
        bool selfClosingTag = false;
        string startTag = string.Empty;
        string tabSpace = "\t";

        #endregion Fields

        #region Constructors

        public HtmlFormatter()
            : this(null, null, true, null, null)
        {
        }

        public HtmlFormatter(string tab, List<string> elementsToIgnore)
            : this(tab, elementsToIgnore, true, null, null)
        {
        }

        public HtmlFormatter(string tab, List<string> elementsToIgnore, bool concatScriptBlocks, List<string> scriptIgnoreList)
            : this(tab, elementsToIgnore, concatScriptBlocks, scriptIgnoreList, null)
        {
        }

        public HtmlFormatter(string tab, bool concatScriptBlocks)
            : this(tab, null, concatScriptBlocks, null, null)
        {
        }

        public HtmlFormatter(List<string> elementsToIgnore)
            : this(null, elementsToIgnore, true, null, null)
        {
        }

        public HtmlFormatter(string tab, List<string> elementsToIgnore, bool concatScriptBlocks, List<string> scriptIgnoreList, List<string> noFormattingList)
        {
            scriptIdIgnoreMarkerValue = scriptIgnoreList;
            concatScriptBlockValue = concatScriptBlocks;
            noFormattingListValue = noFormattingList;
            if (!string.IsNullOrEmpty(tab))
                tabSpace = tab;
            if (elementsToIgnore != null)
                ignoreElements = new List<string>(elementsToIgnore);
        }

        #endregion Constructors

        #region Properties

        public bool ConcatScriptBlocks
        {
            get
            {
                return concatScriptBlockValue;
            }
        }

        public List<string> NoFormattingList
        {
            get
            {
                if (noFormattingListValue == null)
                    noFormattingListValue = new List<string>();
                return noFormattingListValue;
            }
        }

        public bool RemoveFormattingAndSpaces
        {
            get
            {
                return removeFormattingAndSpacesValue;
            }
            set
            {
                removeFormattingAndSpacesValue = value;
            }
        }

        public List<string> ScriptIdIgnoreMarker
        {
            get
            {
                if (scriptIdIgnoreMarkerValue == null)
                    scriptIdIgnoreMarkerValue = new List<string>();
                return scriptIdIgnoreMarkerValue;
            }
        }

        bool IgnoreLastOpeningTag
        {
            get
            {
                if (openElementsHierarchy.Count == 0) return false;
                string lastElem = openElementsHierarchy[openElementsHierarchy.Count - 1];
                return IgnoreElement(lastElem);
            }
        }

        #endregion Properties

        #region Methods

        public string Parse(char[] input)
        {
            if (openElementsHierarchy == null)
                openElementsHierarchy = new List<string>();
            if (attributes == null)
                attributes = new List<string>();

            StringBuilder output = new StringBuilder();
            int index = 0;
            /*
                HttpResponse filter is provided in chunks, so we could
                have been processing a style chunk or a script chunk last before
                we got cutoff to process a new chunk (which is merily a continuation
                of what we were doing last). So let's continue finishing pending tasks first.
             */
            for (int num1 = index; num1 < input.Length; num1++)
            {
                bool skip = false;
                char c = input[num1];

                string c1 = c.ToString();
                switch (c1)
                {
                    case "<":
                        if (appendToStartElement)
                        {
                            if (beginAttribute)
                            {
                                endAttribute = true;
                                AppendAttribute(AttributePosition.End);
                            }
                            RenderAttributes();
                            output.Append(startTag);
                            appendToStartElement = false;
                        }
                        if (!appendToStartElement)
                        {
                            startTag = c1;
                            appendToStartElement = true;
                            appendToEndElement = false;
                        }
                        else if (!appendToEndElement)
                        {
                            endTag = c1;
                            appendToEndElement = true;
                            appendToStartElement = false;
                        }
                        break;
                    case ">":
                        if (appendToStartElement)
                        {
                            if (beginAttribute)
                            {
                                endAttribute = true;
                                AppendAttribute(AttributePosition.End);
                            }
                            RenderAttributes();
                            startTag += c1;
                            if (startTag.IndexOf("/>") != -1)
                            {
                                // we have a self closing element without
                                // content, so we've reached the closing tag
                                appendToEndElement = false;
                                selfClosingTag = true;
                            }
                            appendToStartElement = false;
                            bool isAllowedStartElement = IsAllowedStartElement(startTag);
                            bool formatStartTag = FormatStartTag();
                            if (isAllowedStartElement)
                            {
                                if (!AppendScriptBlock(startTag))
                                {
                                    if (!ignore && formatStartTag)
                                        RenderFormatting(ref output);
                                    output.Append(startTag);
                                }
                                if (!ignore && IgnoreElement(startTag))
                                    ignore = true;
                            }
                            string openingTag = string.Format("<{0}>", ParseElementName(startTag));
                            if (!selfClosingTag)
                                openElementsHierarchy.Add(openingTag);
                            location = CharacterLocation.InnerHTML;
                            if (!selfClosingTag)
                                ++indent;
                            skip = true;
                            selfClosingTag = false;
                        }
                        else if (appendToEndElement)
                        {
                            endTag += c1;
                            appendToEndElement = false;

                            if (openElementsHierarchy.Count > 0)
                                --indent;

                            bool isAllowedEndElement = IsAllowedEndElement(endTag);
                            bool formatEndTag = FormatEndTag();
                            if (ClosingElementMatchesStartTag(endTag))
                            {
                                if (isAllowedEndElement)
                                {
                                    if (!AppendScriptBlock(endTag))
                                    {
                                        AppendAllScriptBeforeBodyEnd(endTag, ref output);
                                        if (!ignore && formatEndTag)
                                            RenderFormatting(ref output);

                                        output.Append(endTag);
                                        addToScriptBlock = false;
                                    }
                                    else if (IsScriptEndTag(endTag))
                                    {
                                        addToScriptBlock = false;
                                        if (!moveScriptBlock)
                                        {
                                            if (!ignore)
                                                RenderFormatting(ref output);
                                            output.Append(endTag);
                                            moveScriptBlock = true;
                                        }
                                    }
                                }
                                if (openElementsHierarchy.Count > 0)
                                    openElementsHierarchy.RemoveAt(openElementsHierarchy.Count - 1);
                                else
                                    location = CharacterLocation.OuterHTML;
                            }
                            else if (isAllowedEndElement)
                            {
                                if (addToScriptBlock)
                                {
                                    scriptBlocks.Append(endTag);
                                    skip = true;
                                    break;
                                }
                                string openingTag = string.Format("<{0}>", ParseElementName(endTag));
                                if (!AppendScriptBlock(endTag))
                                {
                                    --indent;
                                    if (!ignore && formatEndTag)
                                        RenderFormatting(ref output);

                                    output.Append(endTag);

                                    ++indent;
                                }
                                location = CharacterLocation.OuterHTML;
                            }
                            if (ignore && IgnoreElement(endTag))
                                ignore = false;
                            skip = true;
                        }
                        break;
                    case "/":
                        if (lastChar == "<" && appendToStartElement)
                        {
                            appendToStartElement = false;
                            appendToEndElement = true;
                            endTag = startTag;
                        }
                        if (appendToEndElement)
                        {
                            endTag += c1;
                        }
                        else if (appendToStartElement && !ignore)
                        {
                            if (endAttribute)
                            {
                                RenderAttributes();
                                startTag += string.Format(" {0}", c1);
                            }
                            else
                            {
                                if (beginAttribute)
                                    attribute += c1;
                                else
                                    startTag += c1;
                                AppendAttribute(AttributePosition.Begin);
                            }
                        }
                        break;
                    default:
                        if (appendToStartElement)
                        {
                            if (IsNotNewElementStart(startTag) ||
                                ((startTag == "<" || startTag == "</") && ignore))
                            {
                                appendToStartElement = false;
                                if (!addToScriptBlock)
                                    output.Append(startTag);
                                else
                                    scriptBlocks.Append(startTag);
                                if (startTag == "<!")
                                    location = CharacterLocation.InnerHTML;
                            }
                            else
                            {
                                if (!beginAttribute && CharIsWhiteSpace(c))
                                    beginAttribute = true;
                                else if (beginAttribute && CharIsWhiteSpace(c))
                                    endAttribute = true;

                                if (beginAttribute)
                                    attribute += c1;
                                else
                                    startTag += c1;
                                AppendAttribute(AttributePosition.Begin);
                            }
                        }
                        else if (appendToEndElement)
                        {
                            endTag += c1;
                        }
                        break;
                }
                if ((!appendToStartElement && !appendToEndElement) &&
                    !skip)
                {
                    if (addToScriptBlock)
                        scriptBlocks.Append(c);
                    else if (ignore)
                        output.Append(c);
                    else if (location == CharacterLocation.OuterHTML && !CharIsWhiteSpace(c))
                        output.Append(c);
                    else if (location == CharacterLocation.InnerHTML && !IsFormattingChar(c))
                        output.Append(c);
                }
                lastChar = c1;//in case we want to know the last char :-)
            }
            return output.ToString();
        }

        bool AllowFormatScriptBlock(string tag)
        {
            if (ScriptIdIgnoreMarker.Count == 0) return true;
            foreach (string value in ScriptIdIgnoreMarker)
            {
                string f = string.Format("id=\"{0}\"", value);
                if (tag.IndexOf(f, StringComparison.OrdinalIgnoreCase) != -1)
                    return false;
            }
            return true;
        }

        void AppendAllScriptBeforeBodyEnd(string tag, ref StringBuilder output)
        {
            if (tag.Equals("</body>", StringComparison.OrdinalIgnoreCase))
            {
                if (scriptInclude.Count > 0)
                {
                    RenderFormatting(ref output);
                    output.Append(string.Join("\n", scriptInclude.ToArray()));
                }
                if (scriptBlocks.Length > 0)
                {
                    RenderFormatting(ref output);
                    output.Append(@"<script type=""text/javascript"">");
                    output.Append(scriptBlocks.ToString());
                    RenderTabs(ref output);
                    output.Append(@"</script>");
                }
            }
        }

        void AppendAttribute(AttributePosition position)
        {
            if (endAttribute)
            {
                string attributeName = ParseAttributeName(attribute);
                if (IsAllowedAttribute(startTag, attributeName))
                    attributes.Add(attribute.Trim());
                attribute = string.Empty;//reset
                if (position == AttributePosition.End)
                    beginAttribute = false;
                endAttribute = false;
            }
        }

        bool AppendScriptBlock(string tag)
        {
            if (!ConcatScriptBlocks) return false;

            if (IsScriptIncludeOrBlock(tag))
            {
                if (!AllowFormatScriptBlock(tag))
                {
                    moveScriptBlock = false;
                    return false;
                }
                if (tag.IndexOf("src=",
                    StringComparison.OrdinalIgnoreCase) != -1)
                    scriptInclude.Add(string.Format("{0}</script>", tag));
                else
                    addToScriptBlock = true;
                return true;
            }
            return false;
        }

        bool CharIsWhiteSpace(char c)
        {
            return char.IsWhiteSpace(c);
        }

        bool ClosingElementMatchesStartTag(string match)
        {
            /*
                find closing tag return to caller the correct one. in case it is wrong,
             * then the caller should be able to use the right closing tag and self close.
             */
            if (string.IsNullOrEmpty(match) || openElementsHierarchy.Count == 0) return false;
            string lastElem = openElementsHierarchy[openElementsHierarchy.Count - 1];
            string elementNameMatch = ParseElementName(match);
            string elementName = ParseElementName(lastElem);
            if (elementNameMatch.Equals(elementName, StringComparison.OrdinalIgnoreCase))
                return true;
            return false;
        }

        bool FormatEndTag()
        {
            if (openElementsHierarchy.Count > 1 && NoFormattingList.Count > 0)
            {
                string endNode = openElementsHierarchy[openElementsHierarchy.Count - 2];
                endNode = string.Format("</{0}", endNode.Substring(1));
                foreach (string nodeName in NoFormattingList)
                {
                    string match = string.Format("</{0}", nodeName.Substring(1));
                    if (endNode.StartsWith(match, StringComparison.OrdinalIgnoreCase))
                        return false;
                }
            }
            return true;
        }

        bool FormatStartTag()
        {
            if (openElementsHierarchy.Count > 0 && NoFormattingList.Count > 0)
            {
                string startNode = openElementsHierarchy[openElementsHierarchy.Count - 1];
                foreach (string nodeName in NoFormattingList)
                {
                    if (startNode.StartsWith(nodeName, StringComparison.OrdinalIgnoreCase))
                        return false;
                }
            }
            return true;
        }

        void HasSkipMarker(string tag)
        {
        }

        bool IgnoreElement(string tag)
        {
            string tagName = ParseElementName(tag);
            foreach (string elem in ignoreElements)
            {
                if (elem.Equals(tagName, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            return false;
        }

        bool IsAllowedAttribute(string startTag, string attributeName)
        {
            return true;
        }

        bool IsAllowedEndElement(string match)
        {
            return true;
        }

        bool IsAllowedStartElement(string match)
        {
            return true;
        }

        bool IsFormattingChar(char match)
        {
            /*
                 \f form feed
                 \n new line
                 \r carriage return
                 \t horizontal tab
                 */
            char[] chars = new char[] { '\f', '\n', '\r', '\t' };
            foreach (char c in chars)
            {
                if (c.CompareTo(match) == 0)
                    return true;
            }
            return false;
        }

        bool IsNotNewElementStart(string tag)
        {
            if (tag.StartsWith("<!"))
                return true;
            return false;
        }

        bool IsScriptBlock(string tag)
        {
            if (tag.StartsWith("<script", StringComparison.OrdinalIgnoreCase) &&
                tag.IndexOf("src=", StringComparison.OrdinalIgnoreCase) != -1)
                return true;
            return false;
        }

        bool IsScriptEndTag(string tag)
        {
            return tag.StartsWith("</script", StringComparison.OrdinalIgnoreCase);
        }

        bool IsScriptInclude(string tag)
        {
            if (tag.StartsWith("<script", StringComparison.OrdinalIgnoreCase) &&
                tag.IndexOf("src=", StringComparison.OrdinalIgnoreCase) == -1)
                return true;
            return false;
        }

        bool IsScriptIncludeOrBlock(string tag)
        {
            return (tag.StartsWith("<script", StringComparison.OrdinalIgnoreCase) ||
                tag.StartsWith("</script", StringComparison.OrdinalIgnoreCase));
        }

        string ParseAttributeName(string value)
        {
            if (string.IsNullOrEmpty(value)) return string.Empty;
            string[] pairs = value.Split('=');
            return (pairs[0] ?? string.Empty).Trim();
        }

        string ParseElementName(string element)
        {
            string elementName = string.Empty;
            int startLen = 2;
            int start = element.IndexOf("</");
            if (start == -1)
            {
                startLen = 1;
                start = element.IndexOf("<");
            }
            if (start != -1)
            {
                int end = element.IndexOf(" ");
                if (end == -1)
                    end = element.IndexOf(">");
                if (end != -1)
                {
                    start += startLen;
                    elementName = element.Substring(start, end - start);
                }
            }
            return elementName;
        }

        void RenderAttributes()
        {
            if (attributes.Count > 0)
            {
                startTag = string.Format("{0} {1}", startTag.Trim(), string.Join(" ", attributes.ToArray()));
                attributes.Clear();
            }
        }

        void RenderFormatting(ref StringBuilder output)
        {
            if (RemoveFormattingAndSpaces) return;
            output.Append('\n');
            RenderTabs(ref output);
        }

        void RenderTabs(ref StringBuilder output)
        {
            for (int num1 = 0; num1 < indent; num1++)
            {
                output.Append(tabSpace);
            }
        }

        #endregion Methods
    }
}